﻿//******************************************************************************************************
//  Receiver.cs - Gbtc
//
//  Copyright © 2010, Grid Protection Alliance.  All Rights Reserved.
//
//  Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
//  the NOTICE file distributed with this work for additional information regarding copyright ownership.
//  The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may
//  not use this file except in compliance with the License. You may obtain a copy of the License at:
//
//      http://www.opensource.org/licenses/eclipse-1.0.php
//
//  Unless agreed to in writing, the subject software distributed under the License is distributed on an
//  "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
//  License for the specific language governing permissions and limitations.
//
//  Code Modification History:
//  ----------------------------------------------------------------------------------------------------
//  07/07/2010 - Stephen C. Wills
//       Generated original version of source code.
//
//******************************************************************************************************

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using TVA;
using TVA.Communication;
using TVA.PhasorProtocols;
using TVA.PhasorProtocols.IeeeC37_118;

namespace TestBenchApi
{
    /// <summary>
    /// Represents a receiver used to receive data from a concentrator's output stream.
    /// </summary>
    public class Receiver
    {

        #region [ Members ]

        // Events

        /// <summary>
        /// Triggered when a <see cref="Receiver"/> needs to display an error message to the user without failing.
        /// </summary>
        public event EventHandler<EventArgs<string>> DisplayErrorMessage;

        // Fields

        private TransportProtocol m_inputProtocol;
        private int m_port;
        private string m_dataFile;
        private string m_timestampFile;
        private int m_commandChannelPort;
        private string m_concentratorHostName;
        private ushort m_concentratorAccessId;

        private TcpClient m_commandChannel;
        private IClient m_dataChannel;
        private byte[] m_commandChannelBuffer;
        private byte[] m_dataChannelBuffer;
        private BinaryWriter m_dataFileWriter;
        private BinaryWriter m_timestampFileWriter;
        bool m_enabled;

        #endregion

        #region [ Methods ]

        /// <summary>
        /// Initializes the receiver.
        /// </summary>
        /// <param name="settings">The settings needed to initialize the receiver.</param>
        public void Initialize(Dictionary<string, string> settings)
        {
            // Load settings.
            m_inputProtocol = (TransportProtocol)Enum.Parse(typeof(TransportProtocol), settings["inputProtocol"]);
            m_port = int.Parse(settings["port"]);
            m_dataFile = settings["dataFile"];
            m_timestampFile = settings["timestampFile"];
            m_commandChannelPort = int.Parse(settings["commandChannelPort"]);
            m_concentratorHostName = settings["concentratorHostName"];
            m_concentratorAccessId = ushort.Parse(settings["concentratorAccessId"]);

            // Initialize components.
            InitializeCommandChannel();
            InitializeDataChannel();
            InitializeOutputFiles();
        }

        /// <summary>
        /// Starts the receiver.
        /// </summary>
        public void Start()
        {
            if (!m_enabled)
            {
                CommandFrame configFrameCommand = new CommandFrame(m_concentratorAccessId, DeviceCommand.SendConfigurationFrame2, 1);
                CommandFrame enableDataCommand = new CommandFrame(m_concentratorAccessId, DeviceCommand.EnableRealTimeData, 1);
                int numPolls = 0;

                m_enabled = true;
                m_dataFileWriter = new BinaryWriter(new FileStream(m_dataFile, FileMode.Append, FileAccess.Write));
                m_timestampFileWriter = new BinaryWriter(new FileStream(m_timestampFile, FileMode.Append, FileAccess.Write));

                // If the command channel and data channel are the same,
                // the data channel will be started when the command channel is started.
                if (m_commandChannel != m_dataChannel)
                    m_dataChannel.ConnectAsync();

                m_commandChannel.ConnectAsync().WaitOne();

                // Poll the state of the command channel until the connection has succeeded or failed.
                while (m_commandChannel.CurrentState == ClientState.Connecting && numPolls < 10)
                {
                    numPolls++;
                    Thread.Sleep(100);
                }

                // If we managed to connect to the command channel, send the commands to receive data.
                // If we didn't, assume the concentrator isn't ready.
                //  This means the test needs to be run again when the concentrator is ready.
                if (m_commandChannel.CurrentState == ClientState.Connected)
                {
                    m_commandChannel.SendAsync(configFrameCommand.BinaryImage).WaitOne();
                    m_commandChannel.SendAsync(enableDataCommand.BinaryImage).WaitOne();
                }
                else
                {
                    string errorMessage = "The concentrator is not ready to connect to the PDC Test Bench."
                        + " You will need to restart the test in order to receive data from the concentrator.";

                    OnDisplayErrorMessage(errorMessage);
                }
            }
        }

        /// <summary>
        /// Stops the recevier.
        /// </summary>
        public void Stop()
        {
            if (m_enabled)
            {
                m_enabled = false;

                m_commandChannel.Disconnect();
                m_dataChannel.Disconnect();
                m_dataFileWriter.Close();
                m_timestampFileWriter.Close();

                // Prepend unexpected frame timestamps to timestamp file.
                Generator.PrependUnexpectedTimestamps(m_timestampFile);
            }
        }

        // Initializes the command channel.
        private void InitializeCommandChannel()
        {
            string connectionString = string.Format("server={0}:{1}", m_concentratorHostName, m_commandChannelPort);
            m_commandChannel = new TcpClient(connectionString);
            m_commandChannel.ReceiveDataComplete += m_commandChannel_ReceiveDataComplete;
        }

        // Initializes the data channel.
        private void InitializeDataChannel()
        {
            string connectionString = string.Format("port={0}", m_port);

            if (m_inputProtocol == TransportProtocol.Tcp)
                m_dataChannel = m_commandChannel;
            else
            {
                m_dataChannel = new UdpClient(connectionString);
                m_dataChannel.ReceiveDataComplete += m_dataChannel_ReceiveDataComplete;
            }
        }

        // Initializes the data file and timestamp file used for output.
        private void InitializeOutputFiles()
        {
            if (File.Exists(m_dataFile))
                File.Delete(m_dataFile);

            if (File.Exists(m_timestampFile))
                File.Delete(m_timestampFile);
        }

        // This method will attempt to connect to the command channel and send the configuration frame request.
        private void ConnectToCommandChannel(object state)
        {
            CommandFrame configFrameCommand = new CommandFrame(m_concentratorAccessId, DeviceCommand.SendConfigurationFrame2, 1);
            CommandFrame enableDataCommand = new CommandFrame(m_concentratorAccessId, DeviceCommand.EnableRealTimeData, 1);

            m_commandChannel.ConnectAsync().WaitOne();
            m_commandChannel.SendAsync(configFrameCommand.BinaryImage).WaitOne();
            m_commandChannel.Send(enableDataCommand.BinaryImage);

            while (m_enabled && m_commandChannel.CurrentState != ClientState.Connected)
            {
                Thread.Sleep(500);

                if (m_commandChannel.CurrentState == ClientState.Disconnected)
                    m_commandChannel.Connect();
            }

            if (m_enabled)
            {
                m_commandChannel.SendAsync(configFrameCommand.BinaryImage).WaitOne();
                m_commandChannel.Send(enableDataCommand.BinaryImage);
            }
        }

        // Called when data is received through the command channel.
        private void m_commandChannel_ReceiveDataComplete(object sender, EventArgs<byte[], int> e)
        {
            ReceiveDataComplete(ref m_commandChannelBuffer, e.Argument1, e.Argument2);
        }

        // Called when data is received through the data channel.
        private void m_dataChannel_ReceiveDataComplete(object sender, EventArgs<byte[], int> e)
        {
            ReceiveDataComplete(ref m_dataChannelBuffer, e.Argument1, e.Argument2);
        }

        // Processes data received from the concentrator.
        private void ReceiveDataComplete(ref byte[] buffer, byte[] data, int length)
        {
            long timeReceived = PrecisionTimer.UtcNow.Ticks;
            int offset = 0;

            // If there is data in the buffer, prepend it to the received data.
            if (buffer != null && buffer.Length > 0)
            {
                data = buffer.Combine(0, buffer.Length, data, 0, length);
                length = data.Length;
                buffer = null;
            }

            // Loop until all whole frames have been written to the output files.
            while (length >= CommonFrameHeader.FixedLength)
            {
                CommonFrameHeader header = new CommonFrameHeader(null, data, offset);
                int frameLength = header.FrameLength;

                if (length < frameLength)
                    break;
                else
                {
                    WriteToOutputFiles(timeReceived, data, offset, frameLength);
                    offset += frameLength;
                    length -= frameLength;
                }
            }

            buffer = data.BlockCopy(offset, length);
        }

        // Writes to the data file and the timestamp file.
        private void WriteToOutputFiles(long timestamp, byte[] data, int offset, int length)
        {
            // Lock the data file writer so that the command channel and data channel
            // are not both modifying the files at the same time.
            lock (m_dataFileWriter)
            {
                m_dataFileWriter.Write(data, offset, length);
                m_timestampFileWriter.Write(timestamp);
            }
        }

        // Triggers the DisplayErrorMessage event.
        private void OnDisplayErrorMessage(string errorMessage)
        {
            if (DisplayErrorMessage != null)
                DisplayErrorMessage(this, new EventArgs<string>(errorMessage));
        }

        #endregion
        
    }
}
